package gaml.java;

import gaml.error.GamlException;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;

/**
 * creates instances of classes
 * @author shnireck
 *
 */
public class SuperFactory {
	public static final String selfReferenceError = "Constructor with no self-reference not found for class [%s]";
	
	/**
	 * new instance for clazz
	 * @param <T> type of clazz and instance
	 * @param clazz java class, whose instance will be created 
	 * @return new instance of clazz
	 * @throws GamlException errors of instantation
	 */
	public static <T> T getInstance (Class <T> clazz) throws GamlException {
		T instance = null;

		try { instance = clazz.getDeclaredConstructor ().newInstance (); }
		catch (NoSuchMethodException e) {
			if (0 == clazz.getDeclaredConstructors ().length) return getPrimitiveDefault (clazz); 
			
			Constructor <T> constructor = getNoSelfReferenceConstructor (clazz);
			ArrayList <Object> params = getConstructorParams (constructor);

			try { instance = constructor.newInstance (params.toArray (new Object [params.size ()])); }
			catch (Exception ei) { throw new GamlException (ei); }
		}
		catch (SecurityException e) { throw new GamlException (e); }
		catch (IllegalArgumentException e) { throw new GamlException (e); }
		catch (InstantiationException e) { throw new GamlException (e); }
		catch (IllegalAccessException e) { throw new GamlException (e); }
		catch (InvocationTargetException e) { throw new GamlException (e); }

		return instance;
	}
	
	/**
	 * get enum value by it's order
	 * @param <T> type of enum class
	 * @param clazz enum class
	 * @param ordinal order number of enum value
	 * @return value of type T
	 * @throws GamlException if ordinal is not in enum value range
	 */
	public static <T> T getEnum (Class <T> clazz, int ordinal) throws GamlException {
		if (! (clazz.isEnum ())) throw new GamlException (String.format ("[%s] is not an Enumeration!", clazz.getName ()));

		T [] values = clazz.getEnumConstants ();
		if ((0 < ordinal) || (values.length <= ordinal)) throw new GamlException ("Enum instance not found for");
		
		return values [ordinal];
	}
	
	/**
	 * get parameter objects for constructor
	 * @param constructor some constructor
	 * @return array list with parameter objects
	 * @throws GamlException error creating parameter objects
	 */
	protected static ArrayList <Object> getConstructorParams (Constructor <?> constructor) throws GamlException {
		ArrayList <Object> params = new ArrayList <Object> ();
		for (Class <?> pType: constructor.getParameterTypes ())
			params.add (pType.isEnum () ? getEnum (pType, 0) : getInstance (pType));
		
		return params;
	}
	
	/**
	 * among clazz's constructors find first with no self-referenced parameters
	 * @param <T> type of class clazz
	 * @param clazz java class, to find non-selfreferenced constructor for
	 * @return constructor for clazz
	 * @throws GamlException non-selfreferenced constructor not found for class clazz
	 */
	protected static <T> Constructor <T> getNoSelfReferenceConstructor (Class <T> clazz) throws GamlException {
		for (Constructor <?> creator: clazz.getDeclaredConstructors ()) {
			boolean isSelfReferenced = false;
			for (Class <?> pType: creator.getParameterTypes ()) isSelfReferenced = (isSelfReferenced || clazz.equals (pType));
			if (! isSelfReferenced) {creator.setAccessible (true); return (Constructor <T>) creator; }
		}
		
		throw new GamlException (String.format (selfReferenceError, clazz.getName ()));
	}
	
	protected static <T> T getPrimitiveDefault (Class <T> clazz) {
		if (boolean.class.equals (clazz)) return (T) Boolean.valueOf (false);
		else if (char.class.equals (clazz)) return (T) Character.valueOf ('\u0000');
		else if (byte.class.equals (clazz)) return (T) new Byte ("0");
		else if (short.class.equals (clazz)) return (T) Short.valueOf ("0");
		else if (int.class.equals (clazz)) return (T) Integer.valueOf (0);
		else if (long.class.equals (clazz)) return (T) Long.valueOf (0l);
		else if (float.class.equals (clazz)) return (T) Float.valueOf (0);
		else if (double.class.equals (clazz)) return (T) Double.valueOf (0);
		else return null;
	}
}
